﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media.Animation;

namespace Utils.Wpf
{
	public class AnimatorOnVisibilityChange
	{
		#region Attributes

		private static readonly Dictionary<FrameworkElement, bool> _hookedElements;

		#endregion

		#region Constants

		private const int AnimationDuration = 500;

		#endregion

		#region Attached Properties

		public static readonly DependencyProperty AnimationTypeProperty = DependencyProperty.RegisterAttached("AnimationType", typeof(AnimationType), typeof(AnimatorOnVisibilityChange), new FrameworkPropertyMetadata(AnimationType.None, new PropertyChangedCallback(OnAnimationTypePropertyChanged)));

		public static AnimationType GetAnimationType(DependencyObject obj)
		{
			return (AnimationType)obj.GetValue(AnimatorOnVisibilityChange.AnimationTypeProperty);
		}

		public static void SetAnimationType(DependencyObject obj, AnimationType value)
		{
			obj.SetValue(AnimatorOnVisibilityChange.AnimationTypeProperty, value);
		}

		public static readonly DependencyProperty IgnoreFirstTimeProperty = DependencyProperty.RegisterAttached("IgnoreFirstTime", typeof(bool), typeof(AnimatorOnVisibilityChange), new UIPropertyMetadata(false));

		public static bool GetIgnoreFirstTime(DependencyObject obj)
		{
			return (bool)obj.GetValue(AnimatorOnVisibilityChange.IgnoreFirstTimeProperty);
		}

		public static void SetIgnoreFirstTime(DependencyObject obj, bool value)
		{
			obj.SetValue(AnimatorOnVisibilityChange.IgnoreFirstTimeProperty, value);
		}

		public static readonly DependencyProperty DurationProperty = DependencyProperty.RegisterAttached("Duration", typeof(int), typeof(AnimatorOnVisibilityChange), new UIPropertyMetadata(0));

		public static int GetDuration(DependencyObject obj)
		{
			return (int)obj.GetValue(AnimatorOnVisibilityChange.DurationProperty);
		}

		public static void SetDuration(DependencyObject obj, int value)
		{
			obj.SetValue(AnimatorOnVisibilityChange.DurationProperty, value);
		}

		#endregion

		#region Public Methods

		static AnimatorOnVisibilityChange()
		{
			_hookedElements = new Dictionary<FrameworkElement, bool>();

			// Here we "register" on Visibility property "before change" event
			UIElement.VisibilityProperty.AddOwner(typeof(FrameworkElement), new FrameworkPropertyMetadata(Visibility.Visible, AnimatorOnVisibilityChange.VisibilityChanged, AnimatorOnVisibilityChange.CoerceVisibility));
		}

		#endregion

		#region Event Handlers

		private static void OnAnimationTypePropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
		{
			FrameworkElement frameworkElement = dependencyObject as FrameworkElement;

			if (frameworkElement == null)
			{
				return;
			}

			// If AnimationType is set to True on this framework element, 
			if (AnimatorOnVisibilityChange.GetAnimationType(frameworkElement) != AnimationType.None)
			{
				// Add this framework element to hooked list
				AnimatorOnVisibilityChange.HookVisibilityChanges(frameworkElement);
			}
			else
			{
				// Otherwise, remove it from the hooked list
				AnimatorOnVisibilityChange.UnHookVisibilityChanges(frameworkElement);
			}
		}

		private static void VisibilityChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
		{
			// Ignore
		}

		#endregion

		#region Private Methods

		private static void HookVisibilityChanges(FrameworkElement frameworkElement)
		{
			_hookedElements.Add(frameworkElement, false);
		}

		private static void UnHookVisibilityChanges(FrameworkElement frameworkElement)
		{
			if (_hookedElements.ContainsKey(frameworkElement))
			{
				_hookedElements.Remove(frameworkElement);
			}
		}

		private static object CoerceVisibility(DependencyObject dependencyObject, object baseValue)
		{
			// Make sure object is a framework element
			FrameworkElement frameworkElement = dependencyObject as FrameworkElement;
			if (frameworkElement == null)
			{
				return baseValue;
			}

			// Cast to type safe value
			Visibility visibility = (Visibility)baseValue;

			//Debug.WriteLine(DateTime.Now.TimeOfDay.ToString() + ": Requested visibility: " + visibility.ToString() + ". Current visibility: " + frameworkElement.Visibility);
			// If Visibility value hasn't change, do nothing.
			// This can happen if the Visibility property is set using data 
			// binding and the binding source has changed 
			// but the new visibility value hasn't changed.
			if (visibility == frameworkElement.Visibility)
			{
				return baseValue;
			}

			// If element is not hooked by our attached property, stop here
			if (!AnimatorOnVisibilityChange.IsHookedElement(frameworkElement))
			{
				return baseValue;
			}

			// if element has IgnoreFirstTime flag set, then ignore the first time 
			// the property is coerced.
			if (AnimatorOnVisibilityChange.GetIgnoreFirstTime(frameworkElement))
			{
				AnimatorOnVisibilityChange.SetIgnoreFirstTime(frameworkElement, false);
				return baseValue;
			}

			//Debug.WriteLine(DateTime.Now.TimeOfDay.ToString() + ": Updating visibility: " + baseValue.ToString() + " " + dependencyObject.DependencyObjectType.Name);

			// Update animation flag
			// If animation already started - don't restart it (otherwise, infinite loop)
			if (AnimatorOnVisibilityChange.UpdateAnimationStartedFlag(frameworkElement))
			{
				return baseValue;
			}

			// If we get here, it means we have to start fade in or fade out animation. 
			// In any case return value of this method will be Visibility.Visible, 
			// to allow the animation.
			DoubleAnimation doubleAnimation = new DoubleAnimation();
			doubleAnimation.Duration = AnimatorOnVisibilityChange.GetDuration(frameworkElement) > 0
										? new Duration(TimeSpan.FromMilliseconds(AnimatorOnVisibilityChange.GetDuration(frameworkElement)))
										: new Duration(TimeSpan.FromMilliseconds(AnimationDuration));

			// When animation completes, set the visibility value to the requested 
			// value (baseValue)
			doubleAnimation.Completed += (sender, eventArgs) =>
			{
				if (visibility == Visibility.Visible)
				{
					// In case we change into Visibility.Visible, the correct value 
					// is already set
					// So just update the animation started flag
					AnimatorOnVisibilityChange.UpdateAnimationStartedFlag(frameworkElement);
				}
				else
				{
					// This will trigger value coercion again 
					// but UpdateAnimationStartedFlag() function will reture true this time, 
					// thus animation will not be triggered. 
					if (BindingOperations.IsDataBound(frameworkElement, UIElement.VisibilityProperty))
					{
						// Set visiblity using bounded value
						Binding bindingValue = BindingOperations.GetBinding(frameworkElement, UIElement.VisibilityProperty);
						BindingOperations.SetBinding(frameworkElement, UIElement.VisibilityProperty, bindingValue);
					}
					else
					{
						// No binding, just assign the value
						frameworkElement.Visibility = visibility;
					}
				}
			};

			if (visibility == Visibility.Collapsed || visibility == Visibility.Hidden)
			{
				// Fade out by animating opacity
				doubleAnimation.From = (double)frameworkElement.GetValue(UIElement.OpacityProperty);
				doubleAnimation.To = 0.0;
			}
			else
			{
				// Fade in by animating opacity
				doubleAnimation.From = (double)frameworkElement.GetValue(UIElement.OpacityProperty);
				doubleAnimation.To = 1.0;
			}

			// Start animation
			frameworkElement.BeginAnimation(UIElement.OpacityProperty, doubleAnimation);

			// Make sure the element remains visible during the animation
			// The original requested value will be set in the completed event of 
			// the animation
			return Visibility.Visible;
		}

		private static bool IsHookedElement(FrameworkElement frameworkElement)
		{
			return _hookedElements.ContainsKey(frameworkElement);
		}

		private static bool UpdateAnimationStartedFlag(FrameworkElement frameworkElement)
		{
			var animationStarted = _hookedElements[frameworkElement];
			_hookedElements[frameworkElement] = !animationStarted;

			return animationStarted;
		}

		#endregion
	}
}
